package org.bronycorps.mlptss.manager.ai.playerimpl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bronycorps.mlptss.domain.actor.AbstractActor;
import org.bronycorps.mlptss.domain.ai.DatedPolygon;
import org.bronycorps.mlptss.domain.ai.PredictedStage;
import org.bronycorps.mlptss.domain.ai.actor.VirtualActor;
import org.bronycorps.mlptss.domain.layer.DebugLayer;
import org.bronycorps.mlptss.domain.model.BulletType;
import org.bronycorps.mlptss.domain.model.Player;
import org.bronycorps.mlptss.manager.BulletFactory;
import org.bronycorps.mlptss.manager.ai.IArtificialPlayer;
import org.bronycorps.mlptss.manager.ai.PredictionManager;
import org.bronycorps.mlptss.manager.preferences.GameConfiguration;
import org.bronycorps.mlptss.manager.preferences.GameConfiguration.AIConfiguration;
import org.bronycorps.mlptss.utils.VectorUtils;

import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Polygon;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;

public class BasicPlayer implements IArtificialPlayer {

	private class TargetValue {
		float attackValue = 0;
		float distance;
		private float length;
		VirtualActor source;
		int step;

		public TargetValue(VirtualActor source, float distance, int step,
				float length) {
			super();
			this.source = source;
			this.distance = distance;
			this.step = step;
			this.length = length;
		}

	}

	public final static float CONDITION_MINIMUM_POSITION_VALUE = -90000;

	public final static float MINIMUM_POSITION_VALUE = -99999;

	private final static int addToI(int i, int diff) {
		int tmpI = (i - 1 + diff);
		if (tmpI < 0) {
			tmpI += 8;
		}

		return tmpI % 8 + 1;
	}

	private float borderMargin = 30;

	private float bulletSpeed;

	private Vector2 centerPosition = new Vector2(200, 150);

	private float centerRadius = 20;

	private float collisionRadius = 15;

	private DebugLayer debugLayer;
	private boolean enemyOnRange = false;
	private int MaxRecursionSteps = 5;
	private Player player;
	private Polygon[] playerDetectionPaths = new Polygon[9];

	private Integer playerSpeedScale = 1;

	private List<PredictedStage> predictedSituations = new ArrayList<PredictedStage>();
	private float predictionStep = 0.5f;

	private float timer = 0;

	private float visibilityRadius = 80;

	private float weightAttackInRange = 0f;

	private int weightAttackSuccessfulValue = 1;

	private float weightBorderMargin = 1;

	private float weightCenter = 0.01f;

	private int weightChainComboAttackValue = 1;

	private float weightDodgeEnemy = 1;

	private float weightTargetAttack = 5f;

	public BasicPlayer(Player source) {
		super();
		this.debugLayer = DebugLayer.getInstance();
		this.player = source;

		loadConfig();

		createPlayerDetectionPaths();
	}

	private VirtualActor calculateBestTarget(Vector2 position) {

		int step = 1;
		Map<AbstractActor, TargetValue> targets = new HashMap<AbstractActor, TargetValue>();

		List<VirtualActor> actors;
		List<VirtualActor> filteredActors;

		// create detection lines
		final Vector2 bulletUpperDetectionLine = new Vector2(-this.bulletSpeed,
				this.player.getMaxSpeed() * this.playerSpeedScale).nor().mul(
				300);
		final Vector2 bulletDownDetectionLine = bulletUpperDetectionLine.cpy()
				.mul(1, -1);

		final float stepDistance = this.predictionStep * this.bulletSpeed;

		// first, get all actors/enemies with their distance
		for (PredictedStage stage : this.predictedSituations) {
			float deltaTime = this.predictionStep * step;

			actors = stage.getActors();
			// filter actors by their visibility
			filteredActors = filterActorList(position, actors);

			// detect if enemy is in range to be shot
			// ///////////////////////////////////////
			// move detection line
			Vector2 bulletSource = position.cpy().add(
					deltaTime * this.bulletSpeed, 0);
			Vector2 bulletUpLine = bulletUpperDetectionLine.cpy().add(
					bulletSource);
			Vector2 bulletDownLine = bulletDownDetectionLine.cpy().add(
					bulletSource);

			Polygon zoneUp = new Polygon(new float[] { bulletSource.x,
					bulletSource.y, bulletUpLine.x, bulletUpLine.y,
					bulletUpLine.x - stepDistance, bulletUpLine.y,
					bulletSource.x - stepDistance, bulletSource.y });
			Polygon zoneDown = new Polygon(new float[] { bulletSource.x,
					bulletSource.y, bulletDownLine.x, bulletDownLine.y,
					bulletDownLine.x - stepDistance, bulletDownLine.y,
					bulletSource.x - stepDistance, bulletSource.y });

			List<DatedPolygon> enemyHitBoxes = stage.getHitBoxes();
			// this.debugLayer.createPolygon(zoneUp);
			// this.debugLayer.createPolygon(zoneDown);

			List<VirtualActor> detectedActors = new ArrayList<VirtualActor>();
			for (DatedPolygon hitbox : enemyHitBoxes) {
				// check only visible enemies and only those still not in range
				// before
				if (filteredActors.contains(hitbox.getSource())
						&& !targets.containsKey(hitbox.getSource().getSource())) {

					boolean intersect = Intersector.overlapConvexPolygons(
							hitbox, zoneUp);
					intersect |= Intersector.overlapConvexPolygons(hitbox,
							zoneDown);

					if (intersect) {
						// collision found
						float distance = Math.abs(hitbox.p1.x
								- this.player.getPosition().x);
						targets.put(hitbox.getSource().getSource(),
								new TargetValue(hitbox.getSource(), distance,
										step, hitbox.length()));
						detectedActors.add(hitbox.getSource());
					}
				}
			}

			// List<VirtualActor> detectedActors = filterActorsByRange(step,
			// position, targets, filteredActors, deltaTime);

			// calculate actor value based on its life, distance and chain
			// explosion
			if (!detectedActors.isEmpty()) {
				Map<AbstractActor, Float> attackValues = getEnemyAttackValues(
						actors, detectedActors, targets);
				for (AbstractActor key : attackValues.keySet()) {
					targets.get(key).attackValue = attackValues.get(key);
				}
			}

			step++;
		}

		// finally get the best value
		Float bestValue = null;
		VirtualActor bestTarget = null;
		for (TargetValue target : targets.values()) {
			if (bestTarget == null) {
				bestTarget = target.source;
				bestValue = target.attackValue;
			} else {
				if (target.attackValue > bestValue) {
					bestTarget = target.source;
					bestValue = target.attackValue;
				}
			}
		}

		return bestTarget;
	}

	private Object[] calculateNextMove(Vector2 position, Rectangle hitbox,
			PredictionManager predictionManager, float delta, float speed) {
		createSnapshots(predictionManager, delta);

		/*
		 * every value is stored in a grid. The index represent the angle,
		 * including the center. Center is index 0. Then index goes like
		 * 1=0°,2=45°,3=90°...8=315°
		 */
		float[] valuesGrid = new float[9];

		Rectangle hb = new Rectangle(hitbox);
		hb.x += position.x;
		hb.y += position.y + 300;
		// this.debugLayer.createRect(hb);

		float[] resultEnemyAvoidGrid = getEnemyAvoidanceValues(position);

		// avoid margin borders
		if (position.x < this.borderMargin) {
			valuesGrid[4] = -this.weightBorderMargin;
			valuesGrid[5] = -this.weightBorderMargin;
			valuesGrid[6] = -this.weightBorderMargin;
		}
		if (position.y < this.borderMargin) {
			valuesGrid[6] = -this.weightBorderMargin;
			valuesGrid[7] = -this.weightBorderMargin;
			valuesGrid[8] = -this.weightBorderMargin;
		}
		if (position.y > 300 - this.borderMargin) {
			valuesGrid[2] = -this.weightBorderMargin;
			valuesGrid[3] = -this.weightBorderMargin;
			valuesGrid[4] = -this.weightBorderMargin;
		}

		// refuse borders
		if (position.x - speed * delta < this.collisionRadius) {
			valuesGrid[4] = MINIMUM_POSITION_VALUE;
			valuesGrid[5] = MINIMUM_POSITION_VALUE;
			valuesGrid[6] = MINIMUM_POSITION_VALUE;

		}

		// calculate preference for center, if not under attack
		boolean enemyDetected = false;
		for (float f : resultEnemyAvoidGrid) {
			if (f < 0) {
				enemyDetected = true;
				break;
			}
		}
		if (!enemyDetected) {
			Vector2 vectorCenter = this.centerPosition.cpy().sub(position);
			if (vectorCenter.len() > this.centerRadius) {
				final int centerI = Math.round(vectorCenter.angle() / 45) % 8 + 1;
				valuesGrid[centerI] += this.weightCenter;
			}
		}

		this.enemyOnRange = false;
		if (!enemyDetected) {
			VirtualActor target = calculateBestTarget(position);
			if (target != null) {
				// this.debugLayer.createCircle(
				// new Vector2(this.player.getPosition().x, target
				// .getPosition().y + 300), 5f);

				float targetDirection = target.getPosition().y
						- this.player.getPosition().y;
				int i = -1;

				boolean isInFront = this.player.getPosition().x > this.centerPosition.x;

				if (Math.abs(targetDirection) < 5) {
					this.enemyOnRange = true;
					i = isInFront ? 5 : 0;
				} else if (targetDirection > 0) {
					i = isInFront ? 4 : 3;
				} else {
					i = isInFront ? 6 : 7;
				}
				if (i > -1) {
					valuesGrid[i] += this.weightTargetAttack;
				}

			}
		}

		int besti = 0;
		for (int i = 0; i < 9; i++) {
			valuesGrid[i] += resultEnemyAvoidGrid[i] * this.weightDodgeEnemy;
			if (i > 0 && valuesGrid[i] > valuesGrid[besti]) {
				besti = i;
			}
		}

		Vector2 bestPosition = position;
		if (besti > 0) {
			int angle = (besti - 1) * 45;
			bestPosition = new Vector2(speed * delta, 0).rotate(angle).add(
					position);
		}

		return new Object[] { bestPosition, valuesGrid[besti] };
	}

	public void clearSnapshots() {
		this.predictedSituations.clear();
		this.timer = 0;
	}

	private void createPlayerDetectionPaths() {
		this.playerDetectionPaths[0] = new Polygon(new float[] {
				-this.collisionRadius, -this.collisionRadius,
				-this.collisionRadius, this.collisionRadius,
				this.collisionRadius, this.collisionRadius,
				this.collisionRadius, -this.collisionRadius });
		Vector2 origin = new Vector2();

		int counter = 1;
		for (float angle = 0; angle <= 315; angle += 45) {
			Vector2 newPosition = new Vector2(this.visibilityRadius, 0);
			newPosition.rotate(angle);
			this.playerDetectionPaths[counter++] = VectorUtils
					.createOrientedRectangle(origin, newPosition,
							this.collisionRadius);

		}
	}

	private void createSnapshots(PredictionManager predictionManager,
			float delta) {

		final float step = this.predictionStep;

		// for optimization, simulate only the missing steps
		if (this.predictedSituations.isEmpty()) {
			// create all
			for (int i = 0; i < this.MaxRecursionSteps; i++) {
				predictionManager.update(step);
				this.predictedSituations.add(predictionManager.getStage()
						.createSnapshot(this.timer + step * i));
			}
			predictionManager.update(-this.MaxRecursionSteps * step);

			// generate collision boxes
			this.predictedSituations.get(0).generateCollisionBoxes(
					predictionManager.getStage());
			for (int i = 1; i < this.MaxRecursionSteps; i++) {
				this.predictedSituations.get(i).generateCollisionBoxes(
						this.predictedSituations.get(i - 1));
			}

		} else if (this.predictedSituations.get(0).getSnapshotTime() < this.timer) {
			float t0 = this.predictedSituations.get(0).getSnapshotTime();

			// remove the old one and create the new one, as FIFO
			this.predictedSituations.remove(0);

			final float deltaT0 = (this.MaxRecursionSteps * step)
					- (this.timer - t0);

			predictionManager.update(deltaT0);
			this.predictedSituations.add(predictionManager.getStage()
					.createSnapshot(this.timer + deltaT0));

			predictionManager.update(-deltaT0);

			// generate last collision boxes
			this.predictedSituations.get(this.MaxRecursionSteps - 1)
					.generateCollisionBoxes(
							this.predictedSituations
									.get(this.MaxRecursionSteps - 2));
		}
	}

	private List<VirtualActor> filterActorList(Vector2 position,
			List<VirtualActor> source) {

		List<VirtualActor> rejected = new ArrayList<VirtualActor>();

		for (VirtualActor virtualActor : source) {
			if (virtualActor.getPosition().x <= position.x) {
				rejected.add(virtualActor);
			} else if (!rejected.contains(source)) {
				for (VirtualActor comparedActor : source) {
					if (comparedActor != virtualActor
							&& !rejected.contains(comparedActor)
							&& comparedActor.getPosition().x > virtualActor
									.getPosition().x) {
						if (Math.abs(comparedActor.getPosition().y
								- virtualActor.getPosition().y) < virtualActor
								.getRadius()) {
							rejected.add(comparedActor);
						}
					}
				}
			}
		}

		List<VirtualActor> result = new ArrayList<VirtualActor>(source);
		result.removeAll(rejected);

		return result;
	}

	private Map<AbstractActor, Float> getEnemyAttackValues(
			List<VirtualActor> allActors, List<VirtualActor> filteredActors,
			Map<AbstractActor, TargetValue> targets) {
		Map<AbstractActor, Float> result = new HashMap<AbstractActor, Float>();

		// create groups of chain explosions
		List<List<VirtualActor>> groups = new ArrayList<List<VirtualActor>>();

		for (VirtualActor actor : allActors) {
			if (groups.isEmpty()) {
				List<VirtualActor> group = new ArrayList<VirtualActor>();
				group.add(actor);
				groups.add(group);
			} else {
				boolean foundInAGroup = false;
				for (List<VirtualActor> group : groups) {
					boolean foundInGroup = false;
					for (VirtualActor comparedActor : group) {
						float distance = comparedActor.getPosition().cpy()
								.sub(actor.getPosition()).len();
						float radius = Math.max(actor.getRadius(),
								comparedActor.getRadius());
						if (radius * 5 > distance) {
							foundInGroup = true;
							break;
						}
					}
					if (foundInGroup) {
						group.add(actor);
						foundInAGroup = true;
						break;
					}
				}
				if (!foundInAGroup) {
					List<VirtualActor> group = new ArrayList<VirtualActor>();
					group.add(actor);
					groups.add(group);
				}
			}
		}

		// calculate value for each actor, depending on the size of the group
		for (VirtualActor actor : filteredActors) {
			for (List<VirtualActor> group : groups) {
				if (group.contains(actor)) {
					TargetValue target = targets.get(actor.getSource());

					float speedEnemy = target.length / this.predictionStep;
					float timeLeft = target.distance / (speedEnemy);
					float possibleShootCount = timeLeft
							/ this.player.getMaxShootCooldown();
					int life = actor.getSource().getModel().getLife();
					float value = 0;
					if (life > -1) {
						if (possibleShootCount > life) {

							// penalty for the distance to travel, so the
							// closest
							// enemy is preferred
							float distPenalty = Math.abs(this.player
									.getPosition().y
									- target.source.getPosition().y) / 150;

							value = this.weightAttackSuccessfulValue
									- distPenalty;
						}

						value = (value * 10 / life)
								+ this.weightChainComboAttackValue
								* group.size();

						result.put(actor.getSource(), value);

					}
					break;
				}
			}
		}

		return result;
	}

	private float[] getEnemyAvoidanceValues(final Vector2 position) {

		float[] result = new float[9];

		// for each path, check any collision with enemies
		for (int i = 0; i < 9; i++) {
			Polygon path = this.playerDetectionPaths[i];
			path.setPosition(position.x, position.y);

			// draw the detection path
			// Polygon vpp = new Polygon(path.getTransformedVertices());
			// vpp.setPosition(0, 300);
			// this.debugLayer.createPolygon(vpp);

			// check any intersection with the
			int predictionIndex = 0;
			for (PredictedStage situation : this.predictedSituations) {
				for (Polygon poly : situation.getHitBoxes()) {
					if (Intersector.overlapConvexPolygons(path, poly)) {
						updateValueGrid(result, i,
								position.cpy().sub(poly.getX(), poly.getY()),
								predictionIndex);
						// if (((DatedPolygon) poly).getSource() instanceof
						// VirtualIndestructibleActor) {
						// Polygon p = new Polygon(
						// poly.getTransformedVertices());
						// p.translate(0, 300);
						// this.debugLayer.createPolygon(p);
						// }
					}
				}
				predictionIndex++;
			}

		}

		// for (PredictedStage situation : this.predictedSituations) {
		// for (DatedPolygon poly : situation.getHitBoxes()) {
		// if (poly.getSource() instanceof VirtualIndestructibleActor) {
		// Polygon p = new Polygon(poly.getTransformedVertices());
		// // p.translate(0, 300);
		// this.debugLayer.createPolygon(p);
		// }
		// }
		// }

		return result;
	}

	public int getMaxRecursionSteps() {
		return this.MaxRecursionSteps;
	}

	public Vector2 getNextPosition(Vector2 vector2,
			PredictionManager predictionManager, float f, int i) {
		return null;
	}

	@Override
	public Vector2 getNextPosition(Vector2 position, Rectangle hitbox,
			PredictionManager predictionManager, float delta, float speed) {

		Object[] nextMove = calculateNextMove(position, hitbox,
				predictionManager, delta, speed);

		return (Vector2) nextMove[0];
	}

	public float getPredictionStep() {
		return this.predictionStep;
	}

	public float getTimer() {
		return this.timer;
	}

	public float getWeightAttackInRange() {
		return this.weightAttackInRange;
	}

	public float getWeightBorderMargin() {
		return this.weightBorderMargin;
	}

	public float getWeightCenter() {
		return this.weightCenter;
	}

	public float getWeightDodgeEnemy() {
		return this.weightDodgeEnemy;
	}

	public boolean isEnemyOnRange() {
		return this.enemyOnRange;
	}

	private void loadConfig() {
		GameConfiguration config = GameConfiguration.getInstance();

		AIConfiguration aiCfg = config.getAiConfig();
		this.borderMargin = aiCfg.getBorderMargin();
		this.centerPosition = new Vector2(aiCfg.getDefaultPositionX(),
				aiCfg.getDefaultPositionY());
		this.centerRadius = aiCfg.getDefaultPositionRadius();
		this.MaxRecursionSteps = (int) aiCfg.getMaxPredictedSteps();
		this.predictionStep = aiCfg.getPredictionStepDuration();
		this.visibilityRadius = aiCfg.getVisibilityRadius();
		this.weightCenter = aiCfg.getWeightCenter();
		this.weightBorderMargin = aiCfg.getWeightBorder();
		this.weightDodgeEnemy = aiCfg.getWeightDodgeEnemy();

		BulletType bulletType = (BulletType) BulletFactory
				.getBulletType(this.player.getCharacter().getBulletTypeName());
		this.bulletSpeed = bulletType.getSpeed();
		this.playerSpeedScale = config.getInputSpeedScale();
	}

	public void setMaxRecursionSteps(int maxRecursionSteps) {
		this.MaxRecursionSteps = maxRecursionSteps;
	}

	public void setPredictionStep(float predictionStep) {
		this.predictionStep = predictionStep;
	}

	public void setTimer(float timer) {
		this.timer = timer;
	}

	public void setWeightAttackInRange(float weightAttackInRange) {
		this.weightAttackInRange = weightAttackInRange;
	}

	public void setWeightBorderMargin(float weightBorderMargin) {
		this.weightBorderMargin = weightBorderMargin;
	}

	public void setWeightCenter(float weightCenter) {
		this.weightCenter = weightCenter;
	}

	public void setWeightDodgeEnemy(float weightDodgeEnemy) {
		this.weightDodgeEnemy = weightDodgeEnemy;
	}

	@Override
	public void update(float delta) {
		this.timer += delta;
	}

	private void updateValueGrid(float[] result, int i, Vector2 base,
			int predictionIndex) {
		// the detected angle in collision is anyway bad

		final float weight = Math
				.max(0, 1 - base.len() / this.visibilityRadius)
				* (1 - predictionIndex / (this.MaxRecursionSteps * 2));

		final float baseAngle = base.angle();
		final float angle = (i - 1) * 45;
		float diffAngle = angle - baseAngle;
		if (diffAngle > 45) {
			diffAngle -= 360;
		} else if (diffAngle < -45) {
			diffAngle += 360;
		}
		diffAngle /= 45; // normalize the angle
		int dirI = (int) Math.signum(diffAngle);

		result[i] = MINIMUM_POSITION_VALUE;

		// if it's not the center, we can try to avoid it.
		if (i != 0) {

			// reduce center value
			result[0] -= 2 * weight;
			// reduce opposite angle's value
			final int oppositeI = addToI(i, 4);
			result[oppositeI] -= 1 * weight;
			// reduce values to neighbor angles
			final int nextI = addToI(i, 1);
			final int previousI = addToI(i, -1);
			result[nextI] -= 3 * weight;
			result[previousI] -= 3 * weight;

			result[addToI(oppositeI, -dirI)] = -diffAngle * weight;
			result[addToI(oppositeI, -dirI * 2)] = -diffAngle * weight;
		}

	}

}